<template>
  <!-- 视图面板 -->
  <section class="preview" @dragover="dragOver" @drop="drop">
    <div class="preview-head">
      <!-- 顶部工具栏 -->
      <div class="bar">
        <div style="display: inline; position: absolute; left: 20px">{{ actionType }}</div>
        <div style="margin-right: 50px">
          <Tooltip content="清空" placement="bottom-start" @click.native="empty">
            <Icon size="25" type="md-trash" />
          </Tooltip>
          <Tooltip content="全屏" placement="bottom-start" @click.native="fullScreen">
            <Icon size="25" type="md-qr-scanner" />
          </Tooltip>

          <Dropdown>
            <Button type="default">
              <Icon type="ios-phone-portrait" size="20" />
              视图
              <Icon type="ios-arrow-down"></Icon>
            </Button>
            <DropdownMenu slot="list">
              <DropdownItem @click.native="setWidth">调整比例</DropdownItem>
              <DropdownItem @click.native="previewMode = previewMode === 'pc' ? 'mobile' : 'pc'">
                {{ previewMode === 'pc' ? '手机模式' : 'PC模式' }}
              </DropdownItem>
            </DropdownMenu>
          </Dropdown>
          <Tooltip content="编辑样式" placement="bottom-start" @click.native="editStyle">
            <Icon size="25" type="logo-css3" />
          </Tooltip>
          <Tooltip content="查看代码" placement="bottom-start" @click.native="showCode">
            <Icon size="25" type="md-code" />
          </Tooltip>
          <Tooltip
            v-if="$store.state.backupComponents.length"
            content="撤销"
            placement="bottom-start"
            @click.native="undo"
          >
            <Icon size="25" type="ios-return-left" />
          </Tooltip>
        </div>
      </div>
      <!-- 编辑样式 -->
      <div :class="{ content: true, active: actionType !== '预览' }">
        <!-- 组件代码面板 只能查看-->
        <pre v-show="actionType === 'CODE'" v-highlightjs="getSource(components)">
          <code class="html"></code>
        </pre>
        <!-- 样式代码面板  可以编辑-->
        <textarea
          v-show="actionType === '编辑样式'"
          class="css-editor"
          placeholder="请在属性面板class项输入值,如输入为: topTitle, 则这里填写: .topTitle{...}"
          v-model="cssCode"
        ></textarea>
      </div>
    </div>
    <!-- 视图 -->
    <div
      ref="preview"
      v-show="previewMode === 'pc'"
      class="preview-area"
      @click="clickPreview"
      @contextmenu="rightClick"
      @keyup.delete="del"
    >
      <div v-if="!item.parentId" :id="item.info.id" v-for="(item, index) in components"></div>
    </div>
    <iframe src="./#/preview/mobile" class="preview-mobile" v-if="previewMode === 'mobile'"></iframe>
    <div class="preview-tip" v-if="components.length === 0">页面视图区：将左侧组件拖拽进来</div>
    <!-- 组件拖动属于嵌套操作时，这个弹出框会弹出 -->

    <Poptip class="nesting-modal" v-model="showRightModal">
      <div slot="title">
        <div v-if="current && current.info">
          嵌套到
          <span class="strong-text">{{ current.info.name }}:</span>
          中的
        </div>
      </div>
      <div slot="content" class="nesting-modal-content">
        <div
          class="nesting-modal-item"
          v-for="(val, key, index) in current.slots"
          :key="index"
          @click="chooseSlot(key)"
        >
          {{ key }}
        </div>
      </div>
    </Poptip>
    <!-- 右键菜单 -->
    <div ref="contextmenu" :style="contextmenu.style"></div>
    <!-- 右键菜单-打开的操作弹窗 -->
    <mu-popover
      v-if="current.info"
      :anchorOrigin="{ vertical: 'top', horizontal: 'middle' }"
      :targetOrigin="{ vertical: 'top', horizontal: 'left' }"
      :trigger="contextmenu.trigger"
      :open="contextmenu.open"
      @close="contextmenu.open = false"
    >
      <!-- 右键时,高亮的组件名 -->
      <mu-content-block>{{ this.current.info.name }}</mu-content-block>
      <mu-divider />
      <mu-menu class="contextmenu" @itemClick="contextmenu.open = false" autoWidth>
        <mu-menu-item title="复制" @click="copyComponent" />
        <mu-menu-item title="粘贴" @click="pasteComponent" v-if="$store.state.copiedComponents.length" />
        <mu-menu-item title="删除" @click="del" />
      </mu-menu>
    </mu-popover>
  </section>
</template>
<script>
import mount from './mount'
// 代码高亮样式
import '@/assets/css/highlight/default.css'
import '@/assets/css/highlight/Atom-One-Light.css'
// scoped style插件 ，解决webkit不支持scoped的问题
import scopedCss from 'scopedcss'
import { getTemplate } from './template'
// 深度合并
import mergeDeep from '@/utils/mergeDeep'
//取随机id
import guid from '@/utils/guid'
export default {
  name: 'preview',
  data() {
    return {
      showRightModal: false, //是否显示嵌套弹窗
      // 当前操作类型的文字
      actionType: '预览',
      // 控制右键菜单弹窗
      contextmenu: {
        trigger: null,
        open: false,
        style: {},
      },
      // 控制嵌套弹窗
      popover: {
        trigger: null,
        open: false,
      },
      // 插入位置
      insertPosition: {
        position: null,
        component: null,
      },
      // 视图模式
      previewMode: 'pc',
      info: {}, //dragStart存储的拖拽的那个组件数据
    }
  },
  watch: {
    cssCode(val, oldVal) {
      this.addCssCode()
    },
  },
  computed: {
    screenWidth: {
      //各视图宽度比
      get() {
        return this.$store.state.screenWidth
      },
      set(screenWidth) {
        this.$store.commit('setState', {
          screenWidth,
        })
      },
    },
    components: {
      //组件树 ，预览视图中所有组件
      get() {
        return this.$store.state.components
      },
      set(components) {
        // 改变当前最新的组件为上一次放入的那个组件
        let idList = []
        this.components.forEach((item) => {
          idList.push(item.info.id)
        })
        //components为最新的组件集合，this.components为旧的组件集合
        components.forEach((item, index) => {
          if (!idList.includes(item.info.id)) {
            this.$store.commit('setState', {
              newComponentId: item.info.id,
            })
          }
        })
 

        this.$store.commit('setState', {
          components,
        })
      },
    },
    current: {
      //当前选中组件
      get() {
        return this.$store.state.currentComponent
      },
    },
    // 这里需要都用这种set,get分离的格式,读的时候去vuex中读取,改变的时候需要更新到vuex中去
    cssCode: {
      get() {
        return this.$store.state.css
      },
      set(val) {
        this.$store.commit('setState', {
          css: val,
        })
      },
    },
  },

  mounted() {
    // 渲染服务器上或者本地存储的组件

    this.renderComponent()
    // 拖动组件

    this.moveNode()
  },
  methods: {
    // selectSlot(value) {
    //   return new Promise((resolve, reject) => {
    //     this.showRightModal = false
    //     resolve(value)
    //   })
    // },
    moveNode() {
      /**
       *拖动放置位置的虚线框位置计算,
       *ondrag: 只要一拖动右侧的组件就会触发
       */
      document.ondrag = (e) => {
        //视图区 左边到视窗左边的距离,视图区 上边到视窗上边的距离;
        let preview = this.$refs.preview
        let { left, top } = preview.getBoundingClientRect()
        /**
         * clientX :事件被触发时`鼠标指针`相对于`浏览器视图左上角`的坐标。
         * e.clientX - left: 鼠标相对于预览视图的坐标
         * scrollTop: 返回预览视图的垂直滚动条位置
         */
        let x = e.clientX - left,
          y = e.clientY - top + preview.scrollTop
        // 找到所有没有存在父组件id的组件
        this.components
          .filter((item) => !item.parentId)
          .forEach((item1) => {
            //拖动当前组件时,获取视图区域[高亮]的组件的偏移量
            let { offsetLeft, offsetTop, offsetRight, offsetBottom } = item1.position
            let offset = 5
            /**
             * 如果,不在上一个高亮组件内,则不做操作,
             * x 是鼠标相对于预览视图原点的坐标,
             * x > offsetLeft && x < offsetRight && y > offsetTop && y < offsetBottom,
             * 即鼠标落在上一个高亮组件里面
             */
            if (x < offsetLeft || x > offsetRight || y < offsetTop || y > offsetBottom) {
              return
            }
            //插入到高亮组件的上方
            if (y > offsetTop && y < offsetTop + offset) {
              this.setPlaceholder('上方', item1)
              return
            }
            //插入到高亮组件右方
            if (x < offsetRight && x > offsetRight - offset) {
              this.setPlaceholder('右方', item1)
              return
            }
            //插入到高亮组件下方
            if (y < offsetBottom && y > offsetBottom - offset) {
              this.setPlaceholder('下方', item1)
              return
            }
            //插入到高亮组件左方
            if (x > offsetLeft && x < offsetLeft + offset) {
              this.setPlaceholder('左方', item1)
              return
            }
          })
      }
      /**
       * 只要一拖动右侧的组件就会触发,
       * 由于启用drag后的元素无法获取到mouseup事件，
       * 所以监听body的拖拽事件，以便在鼠标松开后，
       *删除虚线框占位元素
       */
      document.body.ondragover = (e) => {
        e.preventDefault()
      }

      /**
       *ondrop: 拖动右侧的组件放下时触发
       */
      document.body.ondrop = (e) => {
        this.removePlaceholder()
        e.preventDefault()
      }
    },
    renderComponent() {
      /**
       * 通过是否有id来判断渲染云端数据还是本地数据:
       * 读取云端数据,用于将我们的页面分享给别人查看时,
       * 可以通过分享id查询数据库中我们配置的页面数据,再通过这些数据
       * 渲染出我们制作的那个页面
       */
      let id = this.$route.params.id
      let query = new this.$lean.Query('Share')
      if (id) {
        query.get(id).then((share) => {
          // 查看share.json
          let store = share.get('store')
          if (store) {
            // 更新到vuex和localStorage中,后面的操作,使用的数据都依赖于vuex
            this.$store.commit('setState', store)
            //dom没有渲染完成 window._Vue为undefined，加个延迟
            setTimeout(() => {
              this.mount()
            }, 0)
          }
        })
      } else if (localStorage.store) {
        // 读取本地数据
        let store = JSON.parse(localStorage.store)
        // 刷新页面时，将本地数据再次保存到vuex中
        this.$store.commit('setState', store)
        // dom没有渲染完成 window._Vue为undefined，加个延迟

        setTimeout(() => {
          this.mount()
        }, 0)
      }
    },
    /**
     * 调整各视图宽度比,
     * width: vuex中保存的宽度比数组.
     * 将数组的第一项拿出来(shift)放到数组的最后,
     * 实现轮换的效果
     */
    setWidth() {
      let screenWidth = this.screenWidth
      let end = screenWidth.shift()
      screenWidth.push(end)
      this.screenWidth = screenWidth
    },
    dragOver(e) {
      e.preventDefault()
    },
    // 拖动元素,鼠标在视图面板中释放时触发
    drop(e) {
      /**
       * 松开拖放,e是容器元素
       * e.target是容器中会落在的那个目标组件
       * CODE视图的文字拖动也会触发此事件，这里屏蔽掉
       */
      if (e.target.className.indexOf('sound-code') !== -1 || e.target.className.indexOf('hljs') !== -1) {
        return
      }
      /**
       * info 为components组件中dragStart拖拽组件时,存储的组件数据
       */

      let info = JSON.parse(e.dataTransfer.getData('info'))

      // 给拖动过来的每个组件添加唯一的id
      info.id = guid()
      this.info = info
      let component

      // let component, template, attributes, slots
      /**
       * indexOf:返回某个指定的字符串值在字符串中首次出现的位置,没找到返回-1
       * var a = "previe-area"
       * var b = "pre"
       * a.indexOf('preview') //  0这样也算找到
       * b.indexOf('preview') // -1
       */
      /**
       * className为preview表示落在视图区的空白处(非嵌套),
       * id为placeholder(为组件名,即表示落在该组件里面),表示落在各个组件中间(非嵌套),
       * e.target.className.indexOf('preview')存在,非嵌套行为
       * e.target.className.indexOf('preview') === -1,不存在,嵌套行为
       * 所以:isNest为true是嵌套行为
       */
      let isNest = e.target.className.indexOf('preview') === -1 && e.target.id !== 'placeholder'
      //true:嵌套行为
      if (isNest) {
        //当要嵌套组件时，需要先在视图中选中我们要嵌套的父组件
        //预览视图中选中组件后，current的值指向选中组件，为空{}就是未选
        if (!this.current.info) {
          this.$Message.error('请选中要嵌套的目标组件')
          return false
        }
        if (!Object.keys(this.current.slots).length) {
          this.$Message.error('不支持嵌套')
          return false
        }
        // 触发当前被嵌套的组件的弹窗
        this.showRightModal = true
      } else {
        //非嵌套操作
        // 获取拖拽进来的那个组件的组件数据
        component = getTemplate(info)

        if (!component.template) throw '没有这个组件的模板'
        let components = JSON.parse(JSON.stringify(this.components))
        // 插入参考位置为null时，就是默认将拖动组件添加到components最后
        if (!this.insertPosition.position) {
        
          components.push(component)
          this.components = components

          this.mount()
        } else {
    
          // 通过id找到之前要插入的那个组件对象
          let index = components.findIndex((component) => component.info.id === this.insertPosition.component.info.id)
          let position = this.insertPosition.position
          // 如果是下方和右方就添加到组件对象的后面
          if (position === '下方' || position === '右方') {
            index++
          }
          components.splice(index, 0, component)
          //占位虚线框删除后，把这个也重置为null，
          // 清空插入方向和参照组件
          this.insertPosition = {
            component: null,
            position: null,
          }
          this.components = components
          this.refresh()
        }
      }
    },
    // 刷新视图
    refresh() {
      /*当 Vue.js 用 v-for 正在更新已渲染过的元素列表时，
       它默认用 “就地复用” 策略。
       如果数据项的顺序被改变，
       Vue将不是移动 DOM 元素来匹配数据项的顺序，
       而是简单复用此处每个元素。
       出于此，每次components顺序变动后，只好使用清空再重新渲染的方式，否则不能成功渲染更新
      */
      let components = JSON.parse(JSON.stringify(this.components))
      this.components = []
      this.$refs.preview.innerHTML = ''
      //settimeout会等到dom重绘完成后在执行fn
      setTimeout(() => {
        this.components = components

        this.mount()
      }, 0)
    },
    /**
     * 如果当前组件节点有data-component-active属性，则返回节点
     * 否则- 如果有父节点，就去查找父节点
     *     - 如果没有父节点，则返回false
     */
    getComponentNode(node) {
      if (node && node.getAttribute('data-component-active') !== null) {
        return node
      } else {
        if (node.parentElement) {
          return this.getComponentNode(node.parentElement)
        } else {
          return false
        }
      }
    },
    /**
     * 选中视图区的组件，鼠标放下也会触发点击
     * 当要嵌套组件时，需要先在视图中选中我们要嵌套的父组件,
     */
    clickPreview(e) {
      let target = e.target
      e.preventDefault()
      let componentHTML = this.getComponentNode(target)
      if (componentHTML) {
        /**
         * 添加选中效果
         * 将所有data-component-active="true"的节点先还原为空,然后让选中的节点data-component-active属性为true
         */
        let list = document.querySelectorAll('[data-component-active="true"]')
        list.forEach((el) => {
          el.setAttribute('data-component-active', '')
        })
        componentHTML.setAttribute('data-component-active', 'true')
        //---------------------------------------------------------
        // 通过组件id找到组件对象, 保存到vuex
        let currentId = componentHTML.id
        let component = this.components.find((component) => component.info.id === currentId)
        if (component) {
          this.$store.commit('setState', {
            currentComponent: component,
          })
        }
      }
    },
    // 挂载dom，渲染页面
    mount() {
      //挂载及更新视图中组件的位置信息
      let components = JSON.parse(JSON.stringify(this.$store.state.components))
        // console.log(111111,components)
      components
        .filter((component) => !component.parentId)
        .forEach((component) => {
          mount(component.info.id, component).then((vm) => {
            let el = document.getElementById(component.info.id)
            component.position = {
              offsetLeft: el.offsetLeft,
              offsetRight: el.offsetLeft + el.clientWidth,
              offsetTop: el.offsetTop,
              offsetBottom: el.offsetTop + el.clientHeight,
            }
            //每次重新挂载后dom变动，更新scoped style
            this.addCssCode()
          })
        })
      setTimeout(() => {
        this.components = components
      }, 0)
    },
    setPlaceholder(position, component) {
      //添加前删除
      this.removePlaceholder()
      // 创建添加元素
      let placeholder = document.createElement('div')

      placeholder.id = 'placeholder'
      // 当前悬停在视图区的哪个组件的上面
      let el = document.getElementById(component.info.id)
      switch (position) {
        case '上方':
          /**
           * 在el前面插入一个占位节点,视图上的效果就是在参照组件的上方插入,
           * 这时,会生成一个标签,该标签表示当前要落下的组件将要落在的区域
           * <div id="placeholder" class="inline"></div>
           */
          this.$refs.preview.insertBefore(placeholder, el)
          break
        case '右方':
          /**
           * 插入左右两边,意味着会插入到同一行,所以要加行内样式,
           * 而且在视图上的插入到右边和下方,就相当于插入到参照组件和参照组件的下一个组件的中间,
           * 即插入到参照组件的下一个组件的前面
           */
          placeholder.classList.add('inline')
          this.$refs.preview.insertBefore(placeholder, el.nextSibling)
          break
        case '下方':
          this.$refs.preview.insertBefore(placeholder, el.nextSibling)
          break
        case '左方':
          placeholder.classList.add('inline')
          this.$refs.preview.insertBefore(placeholder, el)
          break
      }
      // 保存插入的方向和参照组件
      this.insertPosition = {
        position,
        component,
      }
    },
    removePlaceholder() {
      let placeholder = document.getElementById('placeholder')
      if (placeholder) placeholder.parentElement.removeChild(placeholder)
    },
    chooseSlot(slot) {
      this.showRightModal = false

      let components = JSON.parse(JSON.stringify(this.components))
      // 获取当前视图区选中的组件的下标，即高亮组件的下标
      let index = components.findIndex((item) => item.info.id === this.current.info.id)
      // 拖拽的组件，slot的模式
      let nestComponent = getTemplate(
        this.info,
        {
          slot: slot, //第二项传的是属性，这里相当于只传了属性中的slot这一项：_attr为{slot: 'default'}
        },
        {} //默认传空
      )
      nestComponent.attributes.slot = slot
      // 绑定被嵌套的组件id
      nestComponent.parentId = components[index].info.id

      //getTemplate中#可能#修改了components，所以重新获取
      components = JSON.parse(JSON.stringify(this.components))
      index = components.findIndex((item) => item.info.id === this.current.info.id)

      //设置组件的slots
      let slots = components[index].slots || {}
      slots[slot].push({
        id: nestComponent.info.id,
      })

      components.push(nestComponent)

      //提交一次保存 避免getTemplate中获取nestComponent失败
      this.components = components

      //嵌套后的模板

      let component = getTemplate(this.current.info, this.current.attributes, slots)
      if (!component.template) throw '没有这个组件的模板'

      //最父级模板
      let topComponent = this.getParentComponent(this.current)

      topComponent = getTemplate(topComponent.info, topComponent.attributes, topComponent.slots)
      let topIndex = components.findIndex((c) => c.info.id === topComponent.info.id)

      components[topIndex] = topComponent
      components[index] = mergeDeep(components[index], component)
      //更新模板
      this.components = components

      this.mount()
      // return key
      // 必需，勿删，会在ondrop中被重写
    },
    getSource(components) {
      //预览视图中所有组件的代码
      let code = `<template><section>`
      components
        .filter((component) => !component.parentId)
        .forEach((component) => {
          code += component.template
        })
      code += `\n</section></template>`
      //添加用户编辑的css
      let cssText = this.$store.state.css
      if (cssText) {
        cssText = '\n<style scoped>\n' + cssText
        cssText += '\n</style>\n'
        code += cssText
      }
      code = this.$prettyDom(code)

      /*把组件标签中包含的用户不需要的属性删掉，
              因为它只是为了适应预览视图，
              如，组件默认的“position:fixed”会使组件跑到预览视图外
            */
      code = code.replace(/ style=".*?"/g, '')
      code = code.replace(/ tabIndex=".*?"/g, '')
      code = code.replace(/ id=".*?"/g, '')
      code = code.replace(/ data-component-active/g, '')
      code = code.replace(/\n\n/g, '\n')

      return code
    },
    getParentComponent(component) {
      let components = JSON.parse(JSON.stringify(this.$store.state.components))
      if (component.parentId) {
        return this.getParentComponent(components.find((c) => c.info.id === component.parentId))
      } else {
        return component
      }
    },
    // 删除组件
    async del() {
      let components = await this.$store.dispatch('delComponent', this.current.info.id)
      this.refresh()
    },
    showCode() {
      if (this.actionType === 'CODE') this.actionType = '预览'
      else this.actionType = 'CODE'
    },
    editStyle() {
      if (this.actionType === '编辑样式') this.actionType = '预览'
      else this.actionType = '编辑样式'
    },
    // 添加用户自定义的样式到<style>标签里面
    addCssCode() {
      if (!this.cssCode) return
      let style = document.getElementById('custom-style')
      // 如果有写css代码,但是没有id为custom-layout的节点,就需要先去创建这个css标签
      if (!style) {
        style = document.createElement('style')
        style.id = 'custom-style'
        style.setAttribute('scoped', '')
        style.type = 'text/css'
        // 在预览视图区域插入节点
        this.$refs.preview.appendChild(style)
      }
      // 将用户填写的css代码去创建为一个文本节点,然后插入到<style>标签里面
      let cssText = document.createTextNode(this.cssCode)
      style.innerHTML = ''
      style.appendChild(cssText)
      // 动态添加scoped style
      scopedCss.applyTo(this.$refs.preview)
    },
    //右键菜单
    rightClick(e) {
      this.clickPreview(e)
      // 每次右键时都要先关闭上次的右键弹窗
      this.contextmenu.open = false
      // 通过这个div和mu-popover组件关联,来打开弹窗
      this.contextmenu.trigger = this.$refs.contextmenu
      this.contextmenu.style = {
        position: 'fixed',
        left: e.x + 10 + 'px',
        top: e.y - 10 + 'px',
      }
      // 打开右键弹窗
      this.contextmenu.open = true
    },
    // 全屏
    fullScreen() {
      // 如果全屏了,再次点击就取消全屏
      if (document.webkitIsFullScreen) {
        return document.webkitCancelFullScreen()
      }
      var el = document.body
      //Chrome
      if (el.webkitRequestFullScreen) {
        el.webkitRequestFullScreen()
        el.style.width = window.screen.width + 'px'
        el.style.height = window.screen.height + 'px'
      }
      document.addEventListener(
        'webkitfullscreenchange',
        function (e) {
          if (!document.webkitIsFullScreen) {
            el.style.width = 'inherit'
            el.style.height = 'inherit'
          }
        },
        false
      )
    },
    undo() {
      let components = JSON.parse(JSON.stringify(this.$store.state.backupComponents))
      this.$store.commit('setState', {
        backupComponents: [],
        components,
      })
      this.refresh()
    },
    copyComponent() {
      let current = JSON.parse(JSON.stringify(this.current))
      let getCopiedComponents = (component = current) => {
        let hasChild = Object.keys(component.slots).filter((slot) => component.slots[slot].length).length

        //修改id和parentId
        let parentId = guid()

        //局部的的components可能被child.info.id = guid()所改变，所以重新定义个
        let components = JSON.parse(JSON.stringify(this.components))

        //找到了就重新赋值id,并且能够说明有传component进来，因为被传进来的id都是被改变的，不会被找到
        if (components.find((c) => c.info.id === component.info.id)) {
          component.info.id = parentId
          //被复制的组件的最父级组件
          component.parentId = null
        }
        if (hasChild) {
          let arr = []
          Object.keys(component.slots).forEach((slot) => {
            component.slots[slot].forEach((item) => {
              let child = components.find((c) => c.info.id === item.id)
              //赋予新id
              child.info.id = guid()
              child.parentId = parentId
              item.id = child.info.id
              arr.push.apply(arr, getCopiedComponents(child))
            })
          })
          return [component].concat(arr)
        } else {
          return [component]
        }
      }
      let copiedComponents = getCopiedComponents()
      this.$store.commit('setState', {
        copiedComponents,
      })
      this.$toast('复制成功')
    },
    pasteComponent() {
      let components = JSON.parse(JSON.stringify(this.components))
      let index = components.findIndex((c) => c.info.id === this.current.info.id)
      this.popover.trigger = document.getElementById(this.current.info.id)
      this.popover.open = true
      let selectSlot = new Promise((resolve, reject) => {
        this.chooseSlot = (value) => {
          this.popover.open = false
          resolve(value)
        }
      })
      selectSlot.then((slot) => {
        let copiedComponents = JSON.parse(JSON.stringify(this.$store.state.copiedComponents))
        let pasteComponent = copiedComponents.find((c) => !c.parentId)
        components[index].slots[slot].push({
          id: pasteComponent.info.id,
        })
        pasteComponent.parentId = components[index].info.id
        pasteComponent.slot = slot
        components = components.concat(copiedComponents)
        this.$store.commit('setState', {
          components,
          copiedComponents: [], //粘贴后清除剪切板
        })
        let topComponent = this.getParentComponent(components[index])
        let topIndex = components.findIndex((c) => c.info.id === topComponent.info.id)
        components[topIndex].template = getTemplate(
          components[topIndex].info,
          components[topIndex].attributes,
          components[topIndex].slots
        ).template
        //因为getTemplate中需要最新的components，所以提交两次状态
        this.$store.commit('setState', {
          components,
          copiedComponents: [], //粘贴后清除剪切板
        })
        this.refresh()
      })
    },
    /**
     * 清空会清除以下几项
     */
    empty() {
      this.$store.commit('setState', {
        css: '', //用户编辑的自定义css字符串
        currentComponent: {}, //预览视图的选中组件
        components: [], //预览视图的组件树
        backupComponents: [],
        copiedComponents: [], //剪切板
      })
      this.refresh()
    },
  },
}
</script>
<style lang="less" scoped>
@import '~muse-ui/src/styles/colors.less';
.preview {
  height: inherit;
  position: relative;
  overflow: hidden;
  background: white;
}

.preview-area {
  overflow: auto;
  position: relative;
  height: inherit;
  z-index: 0;
  padding-bottom: 100px;
}

.preview-tip {
  width: 100%;
  text-align: center;
  color: #666;
  position: absolute;
  top: 40%;
  z-index: -1;
}

.preview-head {
  width: 100%;
  top: 0;
  z-index: 2;
  .bar {
    background-color: @grey100;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }
  .content {
    height: 0;
    overflow: auto;
    transition: all 0.2s;
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 0;
    margin-bottom: 0;
    background-color: @grey50;
    user-select: text;
    code {
      background: none;
      font-family: Consolas, Liberation Mono, Menlo, Courier, monospace;
    }
  }
  .content.active {
    height: 95vh;
  }
}

.css-editor {
  border: none;
  background: none;
  width: 100%;
  height: 100%;
  outline: none;
  overflow: auto;
}

.preview-mobile {
  position: absolute;
  width: 375px;
  height: 100%;
  left: 0;
  right: 0;
  margin: auto;
  border: none;
  box-shadow: 0 14px 45px rgba(0, 0, 0, 0.247059), 0 10px 18px rgba(0, 0, 0, 0.219608);
}

.contextmenu > div {
  width: 100%;
}
// 嵌套弹窗
.nesting-modal {
  position: absolute;
  left: 150px;
  top: 100px;
  .strong-text {
    font-weight: bold;
  }
  .nesting-modal-content {
    .nesting-modal-item {
      padding-bottom: 10px;
      border-bottom: 1px solid #ddd;
    }
    .nesting-modal-item:hover {
      cursor: pointer;
    }
  }
}
</style>
